/*
 * Copyright 2007-2008 Michele Mostarda ( michele.mostarda@gmail.com ).
 * All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the 'License');
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an 'AS IS' BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */


package org.asemantics.anthill;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.jivesoftware.smack.Chat;
import org.jivesoftware.smack.Roster;
import org.jivesoftware.smack.RosterEntry;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smackx.MessageEventManager;
import org.jivesoftware.smackx.MessageEventNotificationListener;

import java.util.*;


/**
 * This class defines an <i>Ant</i> Task
 * able to send {@link org.asemantics.anthill.CommandMessage}s
 * to {@link org.asemantics.anthill.CommandListenerTask}s.
 *
 * Here an example usage:
 *
 * <pre>
 *&lt;commander
 *    server="server-host"
 *    port="5222"
 *    security="enabled"
 *    username="user1@server-host"
 *    password="secret"
 *    debugger="false"
 *    compression="false"&gt;
 *    &lt;invoke
 *        name="target"
 *        context="user2@server-host"&gt;
 *        &lt;parameter name="property1" value="value1"/&gt;
 *        &lt;parameter name="property2" value="value2"/&gt;
 *        &lt;parameter name="property3" value="value3"/&gt;
 *    &lt;/invoke&gt;
 *&lt;/commander&gt;
 * </pre>
 *
 * @author Michele Mostarda
 */
public class CommanderTask extends AbstractConnectedTask {

    /**
     * This internal listener is used to receive message delivery status. 
     */
    protected class InternalMessageEventNotificationListener implements MessageEventNotificationListener {

        // The currentMessage has been delivered.
        public void deliveredNotification(String from, String packetID) {
            log("The currentMessage has been delivered (" + from + ", " + packetID + ")", Project.MSG_DEBUG);
        }

        // The currentMessage has been displayed.
        public void displayedNotification(String from, String packetID) {
            log("The currentMessage has been reached the target (" + from + ", " + packetID + ")", Project.MSG_DEBUG);
        }

        // The currentMessage's receiver is composing a reply.
        public void composingNotification(String from, String packetID) {
            // Empty.
        }

        // The currentMessage's receiver is offline.
        public void offlineNotification(String from, String packetID) {
            String errMsg = "The currentMessage's receiver is offline (" + from + ", " + packetID + ")";
            if(currentOperation == null) {
                log(errMsg, Project.MSG_DEBUG);
                return;
            }
            if( currentOperation.isFailOnContextOffline() && currentMessage != null && currentMessage.getPacketID().equals(packetID)) {
                throw new BuildException(errMsg);
            }
        }

        // The currentMessage's receiver cancelled composing a reply.
        public void cancelledNotification(String from, String packetID) {
            // Empty.
        }
    }

    /**
     * The list of operations defined inside this task.
     */
    private final List<RemoteOperation> operations = new ArrayList();

    /**
     * The targets node was specified.
     */
    private boolean inspectAdded;

    /**
     * The end node was specified.
     */
    private boolean endAdded;

    /**
     * The internal notification listener.
     */
    private InternalMessageEventNotificationListener messageNotificationListener;

    /**
     * References the currently processed messenger node during
     * the invocation cycle, or <code>null</code> otherwise.
     */
    private RemoteOperation currentOperation;

    private CommandMessage currentMessage;

    /**
     * Constructor.
     */
    public CommanderTask() {
        log("CommanderTask Construction.", Project.MSG_DEBUG);
    }

    /**
     * Creates a nested invoke node.
     *
     * @return
     */
    public Invoke createInvoke() {
        if(inspectAdded) {
            throw new BuildException("Cannot define both an 'invoke' and an 'inspect' nodes");
        }

        Invoke command = new Invoke();
        operations.add(command);
        return command;
    }

    /**
     * Creates a anested Targets node.
     * @return
     */
    public Inspect createInspect() {
        if( ! operations.isEmpty() ) {
            throw new BuildException("Cannot define both an 'inspect' and an 'invoke' nodes");
        }
        if(inspectAdded) {
            throw new BuildException("Only one inspect node is allowed");
        }

        inspectAdded = true;
        Inspect inspect = new Inspect();
        operations.add(inspect);
        return inspect;
    }

    public End createEnd() {
        if( ! operations.isEmpty() ) {
            throw new BuildException("Cannot define both a command and an inspect node");
        }
        if(inspectAdded) {
            throw new BuildException("Cannot define both a target and 'end' node");
        }
        if(endAdded) {
            throw new BuildException("only one 'end' node is allowed");
        }

        endAdded = true;
        End end = new End();
        operations.add(end);
        return end;
    }

    /**
     * Executes the task after having populated the data structures.
     *
     * @throws BuildException
     */
    public void execute() throws BuildException {

        super.execute();

        // Validate nodes configurations.
        for( RemoteOperation operation : operations) {
            operation.validateConfiguration();
        }

        // Process command nodes.
        for( RemoteOperation operation : operations) {
            currentOperation = operation;

            //TODO: reactivate this:
            // Verify context availability.
            //if( ! verifyContextAvailability( operation.getContext()) ) {
            //  throw new BuildException("The context '" + operation.getContext() + "' is not available");
            //}

            // Send the operation message and wait for response.
            Chat chat = createChat( operation.getContext() );
            try {
                currentMessage = operation.prepareMessage();
                Message response = sendMessageAndWaitForReply(chat, currentMessage);
                log("processed message: " + currentMessage + " with response: " + response.toXML(), Project.MSG_DEBUG);
                CommandResponseMessage responseMessage = CommandResponseMessage.deserializeCommandMessage(response);

                if(CommandResponseMessage.isExceptionRaised(response)) {
                    throw new BuildException(
                            "error during invocation of remote operation: " + operation,
                            CommandResponseMessage.getRemoteException(response)
                    );
                }

                try {
                    currentOperation.processResult(this, responseMessage);
                } catch (Throwable t) {
                    log("error during processing result by remote operation: " + t.getMessage(), Project.MSG_WARN);
                    throw new BuildException(t);
                }

            } finally {
                exitFromChat(chat);
                currentOperation = null;
                currentMessage   = null; 
            }
        }

        // Disconnect.
        log("disconnecting", Project.MSG_INFO);

        disconnect();

        log("end messaging", Project.MSG_INFO);
    }

    /**
     * Overrides the ConnectedTaskBase#getConnection method registering on the
     * returned connection the messageNotificationListener.
     *
     * @return
     */
    protected XMPPConnection getConnection() {
        XMPPConnection conn = super.getConnection();
        if( messageNotificationListener == null ) {
            log("registering currentMessage listener", Project.MSG_DEBUG);
            messageNotificationListener = new InternalMessageEventNotificationListener();
            MessageEventManager messageEventManager = new MessageEventManager(conn);
            messageEventManager.addMessageEventNotificationListener(messageNotificationListener);
            log("done", Project.MSG_DEBUG);
        }
        return conn;
    }

    protected boolean verifyContextAvailability( String ctx) {
        loginIfNeeded();
        Roster roster = getConnection().getRoster();

        Collection<RosterEntry> rEntries = roster.getEntries();
        log("Roster entries {", Project.MSG_DEBUG);
        for(RosterEntry re : rEntries) {
            log("entry: " + re.getName() + ", type: " + re.getType() + ", status: " + re.getStatus(), Project.MSG_DEBUG);
        }
        log("}", Project.MSG_DEBUG);

        Iterator<Presence> iter = roster.getPresences(ctx);
        log("Presences {", Project.MSG_DEBUG);
        while(iter.hasNext()) {
            Presence presence = iter.next();
            log("presence: " + presence.isAvailable(), Project.MSG_DEBUG);
        }
        log("}", Project.MSG_DEBUG);

        Presence presence = roster.getPresence(ctx);
        return presence != null && presence.isAvailable();
        }

    /**
     * Loads the key/value properties map on the current task environment.
     * This method is used to set the {@link CommandResponseMessage} result methods.
     *
     * @param map
     */
    protected void loadPropertiesInProject(Map<String,String> map) {
        Project project = getProject();
        for(Map.Entry<String,String> entry : map.entrySet()) {
            project.setNewProperty(entry.getKey(), entry.getValue());
        }
    }

    /**
     * Loads the inspected names and descriptions on the current task environment using the
     * given parameters.
     *
     * @param targetsProperty the name of the property in which to put the target list.
     * @param targetsSeparator the separator of the inspectAdded list.
     * @param targetDescriptionsProperty the name of the property in which to put the target descrptions list.
     * @param targetDescriptionsSeparator the separator of the target descriptions list.
     * @param tasks a map of type <target> / <targetdesc>.
     */
    protected void loadTasklistPropertiesInProject(
            String targetsProperty,
            String targetsSeparator,
            String targetDescriptionsProperty,
            String targetDescriptionsSeparator,
            Map<String,String> tasks
    ) {
        Project project = getProject();
        StringBuffer targets            = new StringBuffer();
        StringBuffer targetDescriptions = new StringBuffer();
        int counter = 0;
        for(Map.Entry<String,String> entry : tasks.entrySet()) {
            targets.append(entry.getKey());
            targetDescriptions.append(entry.getValue());
            if( counter < tasks.entrySet().size() - 1 ) {
                targets.append(targetsSeparator);
                targetDescriptions.append(targetDescriptionsSeparator);
            }
            counter++;
        }
        project.setNewProperty(targetsProperty,            targets.toString());
        project.setNewProperty(targetDescriptionsProperty, targetDescriptions.toString());
        targets.delete(0, targets.length());
        targetDescriptions.delete(0, targetDescriptions.length());
    }

    /**
     * Loads the inspected remote property names in a specified local property using
     * a given character as separator.
     *
     * @param remotePropertiesProperty
     * @param remotePropertiesSeparator
     * @param environmentMap
     */
    protected void loadRemoteEnvPropertiesInProject(
            String remotePropertiesProperty,
            String remotePropertiesSeparator,
            Map<String,String> environmentMap
    ) {
        Project project = getProject();
        StringBuffer remoteProperties = new StringBuffer();
        int counter = 0;
        for(String prop : environmentMap.keySet()) {
            remoteProperties.append(prop);
            if( counter < environmentMap.keySet().size() - 1 ) {
                remoteProperties.append(remotePropertiesSeparator);
            }
            counter++;
        }
        project.setNewProperty(remotePropertiesProperty, remoteProperties.toString());
        remoteProperties.delete(0, remoteProperties.length());
    }

}
